什么是 JVM?
JVM(Java Virtual Machine),即 Java 虚拟机。
JDK 1.8 中的虚拟机是HotSpot VM,它是 Sun JDK 和 Open JDK 中所带的虚拟机,也是目前使用范围最广的 Java 虚拟机。
HotSpot VM不仅继承了 Sun 之前两款商用虚拟机(Classic VM、Exact VM)的优点(如Exact VM中的准确式内存管理),还拥有自己新的技术优势(如它名称中的HopSpot指的就是它的热点代码探测技术)。
准确式内存管理:虚拟机可以知道内存中某个位置的数据是什么类型。比如内存中有一个 32 位的整数 123456,它到底是一个引用类型指向 123456 的内存地址还是一个数值为 123456 的整数,虚拟机将有能力分辨出来,这样才能在 GC 的时候判断堆上的数据是否还可能被使用
热点代码探测:可以通过执行计数器找出最具有编译价值的代码,然后通知 JIT 编译器以方法为单位进行编译。若一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和 OSR (栈上替换)编译动作。通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间于最佳执行性能中取得平衡,且无需等待本地代码输出才能执行程序,即时编译的时间压力也相对减少,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
什么是 JIT?
即时编译(
Just-in-time compilation,JIT)是动态编译的一种形式,可以提高程序运行效率。
通常,程序有两种运行方式:
- (静态)编译执行:编译执行的程序将文件在执行前全部被翻译为机器码
- (动态)解释执行:解释执行的程序对文件是一句一句边运行边翻译。
即时编译器混合了这二者:虽然还是一句一句编译源代码,但是会将翻译过的代码进行缓存以降低性能损耗。
优点
相对于静态编译代码,即时编译的代码可以处理延迟绑定并增强安全性。
Java 中的 JIT
以前通过javac命令将源代码(.java)编译为字节码(.class)后, JVM 运行字节码文件时,会逐条读入,逐条翻译为机器码。
显而易见,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢。
后来为了提高执行速度,引入了 JIT 技术。在运行时 JIT 会把翻译过的机器码保存起来,以备下次使用,可以提高程序运行效率。如下图:

整个过程如下:
- 源代码(
.java)经javac编译成字节码(.class) - JIT 的环境变量会判断该字节码是不是热点代码(多次调用过的代码)
- 是,由 JIT 编译为具体的机器对应的机器码,执行相对快
- 否,则直接由解释器解释执行,执行相对慢,
- 转化为相应的系统调用,内部类库
- 硬件执行
自动内存管理机制
对 Java 而言,由于虚拟机存在自动内存管理机制,所以不再需要为每一个new操作去编写相关的内存释放的代码。
虽然不容易出现内存泄漏和溢出问题,但是一旦出现内存泄漏和溢出方面的问题,若不了解虚拟机是如何使用内存的,便会束手无策。
因此必须了解下 JVM 。
运行时数据区域
JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,如下图:

这些区域都有各自的用途及创建和销毁时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
程序计数器
程序计数器(Program Counter Register)是一块较小的空间内存,它可以看作是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖此计数器完成。
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任意一个确定的时刻,一个处理器都只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,此类内存区域被称为线程私有的内存。
特点
程序计数器具有如下特点:
- 线程私有的内存区域
- 若线程正在执行的是一个 Java 方法,计数器则记录正在执行的虚拟机字节码指令的地址
- 若执行的是 Native 方法,则此计数器值为空(
Undefined) - 此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何
OutOfMemoryError情况的区域
虚拟机栈
Java 虚拟机栈(Virtual Machine Stacks)描述的是执行 Java 方法的内存模型:每个方法在执行时都会创建一个栈帧(Stack Fram),栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存储了编译期可知的:
- 基本数据类型(
boolean、byte、char、short、int、float、long、double) - 对象引用类型(不等同对象本身,可能是一个指向对象起始地址的引用指针,也可能执行一个代表对象的句柄或其他与此对象相关的位置)
returnAddress类型(指向了一条字节码指令的地址)
对 64 位长度的long和double类型的数据会占用 2 个局部变量空间,其余数据类型只占用 1 个。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,方法运行时不会改变局部变量表的大小。
特点
- 线程私有的,它的生命周期与线程相同
本地方法栈
本地方法栈为虚拟机执行时用到的native方法(一般调用 C 语言编写的方法)
堆
Java 堆(Heap)用来存放对象实例的,几乎所有对象实例都在此分配内存。
特点
Java 堆具有如下特点:
- 在虚拟机启动时创建
- 虚拟机所管理的内存中最大的一块
- 被所有线程共享的内存区域
- 可以处于物理不连续的内存空间中,只要逻辑上是连续的即可
- 内存可以选择固定大小或可扩展
- 垃圾收集器管理的主要区域,也被叫做 GC 堆 。由于垃圾收集器大多采用分代收集算法,因此 Java 堆又可以细分为:
- 年轻代
- 老年代
[
]
如上图,JVM 内存被分成多个独立的部分。
广泛地说,JVM 堆内存被分为两部分——年轻代(Young Generation)和老年代(Old Generation)。
年轻代
年轻代是所有新对象产生的地方。
当年轻代内存空间被用完时,就会触发垃圾回收,该垃圾回收叫做 Minor GC(Young GC)。
年轻代又可被分为三个区域
- 一个 Enden 区
- 两个 Survivor 区
特点
年轻代空间的特点:
- 大多数新建的对象都位于 Eden 区
- 当 Eden 区被对象填满时,就会执行 Minor GC,并把所有存活下来的对象转移到其中一个 Survivor 区
- Minor GC 同样会检查存活下来的对象,并把它们转移到另一个 Survivor 区,因此在一段时间内总会有一个空的 Survivor 区
- 经过多次 GC 周期后,仍然存活下来的对象会被转移到年老代内存空间,这通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的
老年代
年老代内存里包含了长期存活的对象,比较大的对象和经过多次 Minor GC 后依然存活下来的对象。
一般来讲,在老年代内存被占满时会进行垃圾回收,该垃圾收集叫做 Major GC,与 Minor GC 相比其会花费更多的时间。
方法区(永久代 –> 元空间)
方法区(Method Area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,也叫Non-Heap(非堆)。
HotSpot 虚拟中的方法区也被称为永久代(PermGen)。
方法区和永久代的关系很像 Java中 接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。
在 JDK 1.8 后,永久代被移除,永久代中的信息存放在了元空间(MetaSpace)。
元空间没有使用堆内存,而是与堆不相连的本地内存区域。因此,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。
在最底层,JVM 通过 mmap 接口向操作系统申请内存映射,每次申请 2MB 空间,这里是虚拟内存映射,不是真的就消耗了主存的 2MB,只有之后在使用的时候才会真的消耗内存。申请的这些内存放到一个链表中 VirtualSpaceList,作为其中的一个 Node。
在上层,MetaSpace 主要由 Klass Metaspace 和 NoKlass Metaspace 两大部分组成。
- Klass MetaSpace:就是用来存 Klass 的,就是 Class 文件在 JVM 里的运行时数据结构,这部分默认放在 Compressed Class Pointer Space 中,是一块连续的内存区域,紧接着 Heap。Compressed Class Pointer Space 不是必须有的,如果设置了
-XX:-UseCompressedClassPointers,或者-Xmx设置大于 32 G,就不会有这块内存,这种情况下 Klass 都会存在 NoKlass Metaspace 里。 - NoKlass MetaSpace:专门来存 Klass 相关的其他的内容,比如 Method,ConstantPool 等,可以由多块不连续的内存组成。虽然叫做 NoKlass Metaspace,但是也其实可以存 Klass 的内容,上面已经提到了对应场景。
特点
方法区具有如下特点:
- 被所有线程共享的内存区域
- 内存可以选择固定大小或可扩展,除此之外还可选择不实现垃圾收集
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
Class文件中除了有类的版本、字段、方法、接口等描述信息,还有常量池,它用于存放编译期生成的各种字面量和符号引用,在类加载后进入方法区的运行时常量池中存储。
Java 7 之前字符串常量池被放到了 Perm 区,所有被 intern 的 String 都会被存在这里,由于 String.intern 是不受控的,所以 -XX:MaxPermSize 的值也不太好设置,经常会出现 java.lang.OutOfMemoryError: PermGen space 异常,所以在 Java 7 之后常量池等字面量(Literal)、类静态变量(Class Static)、符号引用(Symbols Reference)等几项被移到 Heap 中。
参考资料
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社,2013
文章信息
| 时间 | 说明 |
|---|---|
| 2019-08-13 | 初稿 |
| 2025-01-05 | 结构拆分 |